@@ -0,0 +1,118 @@ |
||
1 |
+require 'date' |
|
2 |
+require 'cgi' |
|
3 |
+module Agents |
|
4 |
+ class PublicTransportAgent < Agent |
|
5 |
+ cannot_receive_events! |
|
6 |
+ description <<-MD |
|
7 |
+ Specify the following user settings: |
|
8 |
+ |
|
9 |
+ * stops (array) |
|
10 |
+ * agency (string) |
|
11 |
+ * alert_window_in_minutes (integer) |
|
12 |
+ |
|
13 |
+ This Agent generates Events based on NextBus GPS transit predictions. First, select an agency by visiting [http://www.nextbus.com/predictor/agencySelector.jsp](http://www.nextbus.com/predictor/agencySelector.jsp) and finding your transit system. Once you find it, copy the part of the URL after `?a=`. For example, for the San Francisco MUNI system, you would end up on [http://www.nextbus.com/predictor/stopSelector.jsp?a=**sf-muni**](http://www.nextbus.com/predictor/stopSelector.jsp?a=sf-muni) and copy "sf-muni". Put that into this Agent's agency setting. |
|
14 |
+ |
|
15 |
+ Next, find the stop tags that you care about. To find the tags for the sf-muni system, for the N route, visit this URL: |
|
16 |
+ [http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=**N**](http://webservices.nextbus.com/service/publicXMLFeed?command=routeConfig&a=sf-muni&r=N) |
|
17 |
+ |
|
18 |
+ The tags are listed as tag="1234". Copy that number and add the route before it, separated by a pipe '|' symbol. Once you have one or more tags from that page, add them to this Agent's stop list. E.g, |
|
19 |
+ |
|
20 |
+ agency: "sf-muni" |
|
21 |
+ stops: ["N|5221", "N|5215"] |
|
22 |
+ |
|
23 |
+ This Agent will generate predictions by requesting a URL similar to the following: |
|
24 |
+ |
|
25 |
+ [http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N|5221&stops=N|5215](http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=sf-muni&stops=N|5221&stops=N|5215) |
|
26 |
+ |
|
27 |
+ Finally, set the arrival window that you're interested in. E.g., 5 minutes. Events will be created by the agent anytime a new train or bus comes into that time window. |
|
28 |
+ |
|
29 |
+ alert_window_in_minutes: 5 |
|
30 |
+ |
|
31 |
+ This memory should get cleaned up when timestamp is older than an hour (or something) so that it doesn't fill up all of the Agent's memory. |
|
32 |
+ MD |
|
33 |
+ |
|
34 |
+ |
|
35 |
+ default_schedule "every_2m" |
|
36 |
+ |
|
37 |
+ event_description <<-MD |
|
38 |
+ Events look like this: |
|
39 |
+ { "routeTitle":"N-Judah", |
|
40 |
+ "stopTag":"5215", |
|
41 |
+ "prediction": |
|
42 |
+ {"epochTime":"1389622846689", |
|
43 |
+ "seconds":"3454","minutes":"57","isDeparture":"false", |
|
44 |
+ "affectedByLayover":"true","dirTag":"N__OB4KJU","vehicle":"1489", |
|
45 |
+ "block":"9709","tripTag":"5840086" |
|
46 |
+ } |
|
47 |
+ } |
|
48 |
+ MD |
|
49 |
+ |
|
50 |
+ def check_url |
|
51 |
+ stop_query = URI.encode(options["stops"].collect{|a| "&stops=#{a}"}.join) |
|
52 |
+ "http://webservices.nextbus.com/service/publicXMLFeed?command=predictionsForMultiStops&a=#{options["agency"]}#{stop_query}" |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ def stops |
|
56 |
+ options["stops"].collect{|a| a.split("|").last} |
|
57 |
+ end |
|
58 |
+ def check |
|
59 |
+ hydra = Typhoeus::Hydra.new |
|
60 |
+ request = Typhoeus::Request.new(check_url, :followlocation => true) |
|
61 |
+ request.on_success do |response| |
|
62 |
+ page = Nokogiri::XML response.body |
|
63 |
+ predictions = page.css("//prediction") |
|
64 |
+ predictions.each do |pr| |
|
65 |
+ parent = pr.parent.parent |
|
66 |
+ vals = {"routeTitle" => parent["routeTitle"], "stopTag" => parent["stopTag"]} |
|
67 |
+ if pr["minutes"] && pr["minutes"].to_i < options["alert_window_in_minutes"].to_i |
|
68 |
+ vals = vals.merge Hash.from_xml(pr.to_xml) |
|
69 |
+ if not_already_in_memory?(vals) |
|
70 |
+ create_event(:payload => vals) |
|
71 |
+ log "creating event..." |
|
72 |
+ update_memory(vals) |
|
73 |
+ else |
|
74 |
+ log "not creating event since already in memory" |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+ end |
|
78 |
+ end |
|
79 |
+ hydra.queue request |
|
80 |
+ hydra.run |
|
81 |
+ end |
|
82 |
+ def update_memory(vals) |
|
83 |
+ add_to_memory(vals) |
|
84 |
+ cleanup_old_memory |
|
85 |
+ end |
|
86 |
+ def cleanup_old_memory |
|
87 |
+ self.memory["existing_routes"] ||= [] |
|
88 |
+ self.memory["existing_routes"].reject!{|h| h["currentTime"].to_time <= (Time.now - 2.hours)} |
|
89 |
+ end |
|
90 |
+ def add_to_memory(vals) |
|
91 |
+ self.memory["existing_routes"] ||= [] |
|
92 |
+ self.memory["existing_routes"] << {"stopTag" => vals["stopTag"], "tripTag" => vals["prediction"]["tripTag"], "epochTime" => vals["prediction"]["epochTime"], "currentTime" => Time.now} |
|
93 |
+ end |
|
94 |
+ def not_already_in_memory?(vals) |
|
95 |
+ m = self.memory["existing_routes"] || [] |
|
96 |
+ m.select{|h| h['stopTag'] == vals["stopTag"] && |
|
97 |
+ h['tripTag'] == vals["prediction"]["tripTag"] && |
|
98 |
+ h['epochTime'] == vals["prediction"]["epochTime"] |
|
99 |
+ }.count == 0 |
|
100 |
+ end |
|
101 |
+ def default_options |
|
102 |
+ { |
|
103 |
+ agency: "sf-muni", |
|
104 |
+ stops: ["N|5221", "N|5215"], |
|
105 |
+ alert_window_in_minutes: 5 |
|
106 |
+ } |
|
107 |
+ end |
|
108 |
+ |
|
109 |
+ def validate_options |
|
110 |
+ errors.add(:base, 'agency is required') unless options['agency'].present? |
|
111 |
+ errors.add(:base, 'alert_window_in_minutes is required') unless options['alert_window_in_minutes'].present? |
|
112 |
+ errors.add(:base, 'stops are required') unless options['stops'].present? |
|
113 |
+ end |
|
114 |
+ def working? |
|
115 |
+ event_created_within?(2) && !recent_error_logs? |
|
116 |
+ end |
|
117 |
+ end |
|
118 |
+end |
@@ -0,0 +1,35 @@ |
||
1 |
+<?xml version="1.0" encoding="UTF-8"?> |
|
2 |
+<body copyright="All data copyright San Francisco Muni 2014."> |
|
3 |
+<predictions agencyTitle="San Francisco Muni" routeTitle="N-Judah" routeTag="N" stopTitle="Judah St & La Playa St" stopTag="5221"> |
|
4 |
+ <direction title="Outbound to Ocean Beach"> |
|
5 |
+ <prediction epochTime="1389707083293" seconds="1668" minutes="27" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1443" block="9705" tripTag="5840326"/> |
|
6 |
+ <prediction epochTime="1389708835605" seconds="3420" minutes="57" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1518" block="9708" tripTag="5840327"/> |
|
7 |
+ <prediction epochTime="1389709795605" seconds="4380" minutes="73" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1404" block="9710" tripTag="5840328"/> |
|
8 |
+ </direction> |
|
9 |
+ <direction title="Outbound to Ocean Beach via Downtown"> |
|
10 |
+ <prediction epochTime="1389706393991" seconds="978" minutes="16" isDeparture="false" dirTag="N__OB4KJU" vehicle="1543" vehiclesInConsist="2" block="9703" tripTag="5840324"/> |
|
11 |
+ <prediction epochTime="1389706512784" seconds="1097" minutes="18" isDeparture="false" dirTag="N__OB4KJU" vehicle="1476" vehiclesInConsist="2" block="9704" tripTag="5840083"/> |
|
12 |
+ <prediction epochTime="1389707746994" seconds="2331" minutes="38" isDeparture="false" dirTag="N__OB4KJU" vehicle="1507" block="9706" tripTag="5840084"/> |
|
13 |
+ <prediction epochTime="1389708458668" seconds="3043" minutes="50" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1489" block="9707" tripTag="5840085"/> |
|
14 |
+ <prediction epochTime="1389709358668" seconds="3943" minutes="65" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1463" block="9709" tripTag="5840086"/> |
|
15 |
+ </direction> |
|
16 |
+<message text="No Elevator at |
|
17 |
+Van Ness Station"/> |
|
18 |
+</predictions> |
|
19 |
+<predictions agencyTitle="San Francisco Muni" routeTitle="N-Judah" routeTag="N" stopTitle="Judah St & 46th Ave" stopTag="5215"> |
|
20 |
+ <direction title="Outbound to Ocean Beach"> |
|
21 |
+ <prediction epochTime="1389706981164" seconds="1566" minutes="26" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1443" block="9705" tripTag="5840326"/> |
|
22 |
+ <prediction epochTime="1389708733476" seconds="3318" minutes="55" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1518" block="9708" tripTag="5840327"/> |
|
23 |
+ <prediction epochTime="1389709693476" seconds="4278" minutes="71" isDeparture="false" affectedByLayover="true" dirTag="N__OB3" vehicle="1404" block="9710" tripTag="5840328"/> |
|
24 |
+ </direction> |
|
25 |
+ <direction title="Outbound to Ocean Beach via Downtown"> |
|
26 |
+ <prediction epochTime="1389706282012" seconds="866" minutes="14" isDeparture="false" dirTag="N__OB4KJU" vehicle="1543" vehiclesInConsist="2" block="9703" tripTag="5840324"/> |
|
27 |
+ <prediction epochTime="1389706400805" seconds="985" minutes="16" isDeparture="false" dirTag="N__OB4KJU" vehicle="1476" vehiclesInConsist="2" block="9704" tripTag="5840083"/> |
|
28 |
+ <prediction epochTime="1389707635015" seconds="2219" minutes="36" isDeparture="false" dirTag="N__OB4KJU" vehicle="1507" block="9706" tripTag="5840084"/> |
|
29 |
+ <prediction epochTime="1389708346689" seconds="2931" minutes="48" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1489" block="9707" tripTag="5840085"/> |
|
30 |
+ <prediction epochTime="1389709246689" seconds="3831" minutes="63" isDeparture="false" affectedByLayover="true" dirTag="N__OB4KJU" vehicle="1463" block="9709" tripTag="5840086"/> |
|
31 |
+ </direction> |
|
32 |
+<message text="No Elevator at |
|
33 |
+Van Ness Station"/> |
|
34 |
+</predictions> |
|
35 |
+</body> |
@@ -0,0 +1,70 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+describe Agents::PublicTransportAgent do |
|
3 |
+ before do |
|
4 |
+ valid_params = { |
|
5 |
+ "name" => "sf muni agent", |
|
6 |
+ "options" => { |
|
7 |
+ "alert_window_in_minutes" => "20", |
|
8 |
+ "stops" => ['N|5221', 'N|5215'], |
|
9 |
+ "agency" => "sf-muni" |
|
10 |
+ } |
|
11 |
+ } |
|
12 |
+ @agent = Agents::PublicTransportAgent.new(valid_params) |
|
13 |
+ @agent.user = users(:bob) |
|
14 |
+ @agent.save! |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ describe "#check" do |
|
18 |
+ before do |
|
19 |
+ stub_request(:get, "http://webservices.nextbus.com/service/publicXMLFeed?a=sf-muni&command=predictionsForMultiStops&stops=N%7C5215"). |
|
20 |
+ with(:headers => {'User-Agent'=>'Typhoeus - https://github.com/typhoeus/typhoeus'}). |
|
21 |
+ to_return(:status => 200, :body => File.read(Rails.root.join("spec/data_fixtures/public_transport_agent.xml")), :headers => {}) |
|
22 |
+ stub(Time).now {"2014-01-14 20:21:30 +0500".to_time} |
|
23 |
+ end |
|
24 |
+ |
|
25 |
+ it "should create 4 events" do |
|
26 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "should add 4 items to memory" do |
|
30 |
+ @agent.memory.should == {} |
|
31 |
+ @agent.check |
|
32 |
+ @agent.memory.should == {"existing_routes" => [ |
|
33 |
+ {"stopTag"=>"5221", "tripTag"=>"5840324", "epochTime"=>"1389706393991", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
34 |
+ {"stopTag"=>"5221", "tripTag"=>"5840083", "epochTime"=>"1389706512784", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
35 |
+ {"stopTag"=>"5215", "tripTag"=>"5840324", "epochTime"=>"1389706282012", "currentTime"=>"2014-01-14 20:21:30 +0500"}, |
|
36 |
+ {"stopTag"=>"5215", "tripTag"=>"5840083", "epochTime"=>"1389706400805", "currentTime"=>"2014-01-14 20:21:30 +0500"} |
|
37 |
+ ] |
|
38 |
+ } |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ it "should not create events twice" do |
|
42 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
43 |
+ lambda { @agent.check }.should_not change {@agent.events.count} |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ it "should reset memory after 2 hours" do |
|
47 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
48 |
+ stub(Time).now {"2014-01-14 20:21:30 +0500".to_time + 3.hours} |
|
49 |
+ @agent.cleanup_old_memory |
|
50 |
+ lambda { @agent.check }.should change {@agent.events.count}.by(4) |
|
51 |
+ end |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ describe "validation" do |
|
55 |
+ it "should validate presence of stops" do |
|
56 |
+ @agent.options['stops'] = nil |
|
57 |
+ @agent.should_not be_valid |
|
58 |
+ end |
|
59 |
+ |
|
60 |
+ it "should validate presence of agency" do |
|
61 |
+ @agent.options['agency'] = "" |
|
62 |
+ @agent.should_not be_valid |
|
63 |
+ end |
|
64 |
+ |
|
65 |
+ it "should validate presence of alert_window_in_minutes" do |
|
66 |
+ @agent.options['alert_window_in_minutes'] = "" |
|
67 |
+ @agent.should_not be_valid |
|
68 |
+ end |
|
69 |
+ end |
|
70 |
+end |